I created a Tiny Journal app to learn Laravel. Here is the process, step by step.


Most Laravel tutorials give you working code and walk you through it. That’s nice to see how the pieces fit together, but it doesn’t teach you much about what happens when you leave a field blank, type the wrong parameter name, or hesitate before running a command you don’t yet fully trust.

So instead of following a tutorial, I built something small from scratch: Tiny Journal, a single-page journal app. Write an entry, read it, edit it, delete it. No authentication, no tags, no search. Small enough to finish in a weekend, big enough to implement real Laravel concepts along the way.

This is the process I followed, in order, including the pieces that broke.

What you will need

  • Basic knowledge of PHP (variables, functions, arrays)
  • A local Laravel installation.
  • Familiarity with MVC at a conceptual level, no experience in Laravel specifically needed

Step 1: The model and the migration

It all starts with a model, Entryand a migration:

php artisan make:model Entry -m
Schema::create('entries', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('content');
    $table->timestamps();
});
php artisan migrate

two fields, title and contentboth required at the database level. That last detail matters more than it seems it should, it’s the reason the first real bug existed in this project.

Step 2: Read the entries (index and show)

With the table in place, the first functional feature was to simply display entries:

public function index()
{
    $entries = Entry::all();
    return view('entries.index', ('entries' => $entries));
}

public function show(Entry $entry)
{
    return view('entries.show', ('entry' => $entry));
}

show() already uses the route model link here, Laravel matches the {entry} placeholder on the route to the Entry $entry parameter and recovers the record automatically, without manual find() necessary. I didn’t fully appreciate it until later, when I tried to write edit() the long way around and noticed the mismatch (more on that below).

Step 3: Create entries (create and store)

public function create()
{
    return view('entries.create');
}

public function store(Request $request)
{
    $entry = new Entry();
    $entry->title = $request->title;
    $entry->content = $request->content;
    $entry->save();

    return redirect('/');
}

This worked, until I submitted the form with a blank title. What resulted was a full-page database error, a violation of the NOT NULL constraint, straight from the migration in Step 1. The database was correctly applying a rule I had written myself, in just the worst possible place, after the form had already been submitted, with a stack trace instead of a sentence the user could act on.

The solution is validation, called before something touches the database:

public function store(Request $request)
{
    $request->validate((
        'title' => 'required',
        'content' => 'required',
    ));

    $entry = new Entry();
    $entry->title = $request->title;
    $entry->content = $request->content;
    $entry->save();

    return redirect('/');
}

validate() allows the request to pass or stops it and automatically redirects it to the form. To actually show the error, loop through the Laravel error bag in the view:

@foreach ($errors->all() as $error)
    

{{ $error }}

@endforeach

One problem: $errors It’s not a simple array that you can loop through directly, it’s a MessageBagand you need ->all() to remove the strings from the message. loop over $errors and you get nothing, no error, no crash, just silence.

Step 4: Edit entries (edit and update)

public function edit($id)
{
    $entry = Entry::find($id);
    return view('entries.edit', ('entry' => $entry));
}

public function update(Request $request, Entry $entry)
{
    $entry->title = $request->input('title');
    $entry->content = $request->input('content');
    $entry->save();

    return redirect('/');
}

Two things emerged here. First of all, the same blank title bug from Step 3 was still possible in update()since it did not yet have validation. The solution is identical, just sequenced correctly, validate before writing anything to $entrynot after:

public function update(Request $request, Entry $entry)
{
    $request->validate((
        'title' => 'required',
        'content' => 'required',
    ));

    $entry->title = $request->input('title');
    $entry->content = $request->input('content');
    $entry->save();

    return redirect('/');
}

Secondly, comparing edit() and update() side by side clicked binding of the route model. update() uses Entry $entry and never calls find(). edit() I still used the manual $id + Entry::find($id) pattern. Once I rewrote edit() to match:

public function edit(Entry $entry)
{
    return view('entries.edit', ('entry' => $entry));
}

The rule became obvious: the path model link works whenever the URL contains an id that points to something that already exists. That’s true for edit, updateand destroy. It is never true for store either indexThere is no specific record at those URLs to link to in the first place.

Step 5: A more subtle error, old entry

After adding validation, something seemed right for the wrong reason. Write an actual title, leave the content blank, and submit. The page reloads and the title field still shows what I typed. It looked like an old entrance worked. It wasn’t, the title was just $entry->titleintact, because validation stopped the save before anything hit the database.

The real solution is old() assistant:

title) }}">

old('title', $entry->title) First check for leftover entries from a failed send. If it exists, it wins. If the page was opened new, it returns to the stored value of the entry. The real test: change the title, delete the content, send. If connected correctly, your edited title survives the round trip instead of being rolled back.

Step 6: Delete entries and merge six routes into one

public function destroy(Entry $entry)
{
    $entry->delete();
    return redirect('/');
}

At this point I had six handwritten routes, one per action:

Route::get('/entries', (EntryController::class, 'index'));
Route::get('/entries/create', (EntryController::class, 'create'));
Route::post('/entries', (EntryController::class, 'store'));
Route::get('/entries/{entry}/edit', (EntryController::class, 'edit'));
Route::put('/entries/{entry}', (EntryController::class, 'update'));
Route::delete('/entries/{entry}', (EntryController::class, 'destroy'));

Laravel collapses all six into a single line:

Route::resource('entries', EntryController::class);

I sat on this longer than I should have, convinced that the change would force changes elsewhere. It wasn’t like that. The driver didn’t change at all, only the routes file did. Route::resource() It’s not generating new behavior, it’s the same six routes, the same nouns, the same verbs, pre-assembled.

Step 7: Clean with a shared layout

When everything worked, all four views (index, create, edit, show) had their own complete , and “ tags, copied and pasted four times. A shared layout solves this problem:

{{-- resources/views/layouts/app.blade.php --}}



    Tiny Journal
    


    @yield('content')


Each view boils down to its unique content:

{{-- resources/views/entries/index.blade.php --}}
@extends('layouts.app')

@section('content')
    
    @foreach ($entries as $entry)
        

{{ $entry->title }}

{{ $entry->content }}

id }}">View id }}/edit">Edit @endforeach @endsection

Add a stylesheet link to the layout once and each page will automatically select it, without needing to touch any other files.

The walls, briefly

Going back to the previous process, the real friction points were:

  1. A blank title crashes the app (Step 3), the database applies a rule on the wrong layer.
  2. **The same accident hidden in**update() (Step 4), same solution, just needs proper sequencing.
  3. Click on the route model link (Step 4), I once compared a method that used it with one that didn’t.
  4. The previous entry looks like it worked before it actually did. (Step 5), two different mechanisms that produced identical-looking results.
  5. **Doubting before**Route::resource() (Step 6), fear of a change that turned out to be safe.
  6. Four duplicate page shells (Step 7), fixed once the repetition became annoying enough to notice.

None of these were individually difficult. The hard part was realizing, at the time, that a bug or hesitation pointed to a missing concept, not a missing line of code.

Where Tiny Journal Goes From Here

Some obvious next steps if you’re moving forward:

  • flash messages after create, update and delete, so the user gets a confirmation instead of a silent redirect
  • Stricter validation rulesminimum lengths, character limits, rather than just required
  • Soft removalsso “delete” does not mean disappear permanently

If you’re learning Laravel right now: build something small, break it up on purpose, and read the error instead of immediately pasting it into a search bar. The framework usually already has an answer for any repetition or fragility you feel. You just have to hit the wall first to know what question to ask.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *